package com.nulldev.util.web.YouTubeExtractor.extractors.next;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastArrayList;
import com.nulldev.util.data.Date.Dates;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.MarkedForRemoval;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.fuckTracking.FuckTracking;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.formats.AudioCodec;
import com.nulldev.util.web.YouTubeExtractor.formats.FileType;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.Formats;
import com.nulldev.util.web.YouTubeExtractor.formats.VideoCodec;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist;
import com.nulldev.util.web.YouTubeExtractor.signatures.extractor.Extractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo.FormatType;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;

@MarkedForRemoval(reason = "This API no longer functions and is useless. This API is no longer in the test suite.", plannedRemovalWhen = "2.3 or later")
public class YouTubeEmbedNextExtractor extends IYouTubeExtractor {

	/**
	 * DEPRECATED SINCE 2.1.21. MARKED FOR FUTURE REMOVAL.
	 * 
	 * Reason: This API no longer functions and is useless. This API is no longer in
	 * the test suite.
	 */

	private static final Logger logger = LoggerFactory.getLoggerD(
			// Arguments.isArg("--YouTubeExtractor.debug") ||
			// Arguments.isArg("--YouTubeExtractor.NextFEE.debug"),
			true, true);
	public static final String VERSION = "1.0.5";
	private static final boolean DO_NOT_BAIL_ON_BAD_DATA = false;

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		String ytInfoUrl = "https://" + "www.youtube.com/get_video_info?video_id=" + videoID + "&eurl="
				+ LegacyURIUtils.safeEncode("https://youtube.googleapis.com/v/" + videoID) + "&html5=1&c=TVHTML5&cver=6.20180913&gl=US&hl=en";
		if (arguments.containsKey(MAP_TYPE_FORCE_ENGLISH_LANG_COMPAT))
			ytInfoUrl += "&hl=EN";
		return ytInfoUrl;
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		final variable<Integer> _index = new variable<Integer>(-1);
		return ExecutorManager.make(() -> {
			final FastArrayList<YouTubeVideo> results = new FastArrayList<YouTubeVideo>(true);
			logger.debug("Generating URL...");
			final StringBuilder extras = new StringBuilder();
			if (arguments.containsKey(MAP_TYPE_PLAYLIST_STR)) {
				extras.append("&list=");
				extras.append(arguments.get(MAP_TYPE_PLAYLIST_STR));
			}
			if (arguments.containsKey(MAP_TYPE_PLAYLIST_INDEX)) {
				extras.append("&index=");
				final int ix = (int) arguments.get(MAP_TYPE_PLAYLIST_INDEX);
				extras.append(ix);
				_index.set((int) arguments.get(MAP_TYPE_PLAYLIST_INDEX));
			}
			final String url = generateURL(videoID, arguments) + extras;
			Entry[] _args = (!isEnabled(arguments, MAP_TYPE_DISABLE_CONSENT_COOKIE_COMPAT)) ? YouTubeExtractor.DEFAULT_COOKIES : null;
			if (isEnabled(arguments, MAP_TYPE_SEND_FAKE_COOKIES_COMPAT) || isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT)) {
				if (_args == null)
					_args = new Entry[0];
				_args = ArrayUtils.add(_args, new Entry("Cookie", StringsUtil.randomString(64)));
			}
			if (isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT) || isEnabled(arguments, MAP_TYPE_EMULATE_INSECURE_PROXY_COMPAT)) {
				if (_args == null)
					_args = new Entry[0];
				_args = ArrayUtils.addAll(_args, FuckTracking.protect());
			}
			final String result;
			final HttpResponse<String> r;
			try {
				logger.debug("Downloading metadata... [url=" + url + "]");
				r = HttpClient.getString().url(url).timeout(Duration.ofSeconds(15)).headers(_args).create();
				if (r.status() == 403 || r.status() == 404) {
					IOUtils.closeQuietly(r);
					logger.error("[YTE] Error Code: " + r.status());
					return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_VIDEO_NOT_FOUND")
							.setYTMessage("YouTube reports this video as non-existant.");
				}
				result = r.dataThenClose();
			} catch (Exception e) {
				logger.error(e);
				return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA");
			}
			logger.debug("Parsing metadata...");
			logger.debug("Result: " + result);
			final Map<String, String> map = LegacyURIUtils.queryStringToMap(result);
			if (map == null || !map.containsKey("status")) {
				logger.error("Got a null map!");
				logger.debug("Map: " + map);
				return null;
			}
			// url_encoded_fmt_stream_map, adaptive_fmts, status == 'ok'
			if (!map.get("status").equalsIgnoreCase("OK") || map.isEmpty()) {
				logger.error("generateTask() -> status != OK [got=" + map.get("status") + "]");
				return null;
			}
			final UniJsonValue player_response = UniJsonValue.parse(map.get("player_response"), true);
			UniJsonValue videoDetails = player_response.get("videoDetails");
			if (videoDetails == null || videoDetails.isNull()) {
				logger.warn("generateTask() -> videoDetails is null!");
				if (DO_NOT_BAIL_ON_BAD_DATA) {
					if (videoDetails == null)
						videoDetails = UniJsonValue.object();
				} else {
					if (videoDetails == null)
						return new ExtractedVideo(results, null, ExtractionResult.FAILED).setMessage("Failed to get video metadata.");
				}
			}
			logger.debug("Accquiring decipherer.");
			final Executable<Extractor> e = ExecutorManager.globalAsyncSubmit(() -> {
				try {
					return YouTubeExtractor.extractor.getAuto();
				} catch (Exception ef) {
					throw new RuntimeException(ef);
				}
			});
			final List<YouTubeCaptions> captions = new FastArrayList<YouTubeCaptions>(true);
			if (player_response.contains("captions")) {
				// captions found
				final UniJsonValue ct = player_response.getSafe("captions").getSafe("playerCaptionsTracklistRenderer").getIgnoreNull("captionTracks");
				if (ct == null) {
					logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only enabled.
				} else {
					final UniJsonIterableValue captionArray = ct.asArray();
					if (captionArray != null && !captionArray.isNull()) {
						for (final UniJsonValue _v : captionArray) {
							final UniJsonValue caption = _v;
							final String captionURL = caption.getString("baseUrl");
							final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
							final String captionCode = caption.getString("languageCode");
							captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
						}
					}
				}
			}
			Playlist pl = null;
			UniJsonValue keywords = UniJsonValue.array();
			try {
				keywords = videoDetails.get("keywords").asArray();
			} catch (Exception _e) {
			}
			if (keywords == null) {
				keywords = UniJsonValue.array();
			}
			final YouTubeMetadata metadata = new YouTubeMetadata(videoID, videoDetails.getString("title", "Unknown title."),
					videoDetails.getString("author", "Unknown author."), videoDetails.getString("channelId", ""), videoDetails.getLong("viewCount", -1),
					videoDetails.getLong("lengthSeconds", -1), videoDetails.getDouble("averageRating", 5), -1, -1, false, keywords.toArray(), captions);
			if (videoDetails.hasValue("isUpcoming") && videoDetails.getBoolean("isUpcoming")) {
				logger.error("Failed -> This video isn't live yet!");
				final long time = player_response.get("playabilityStatus").get("liveStreamability").get("liveStreamabilityRenderer").get("offlineSlate")
						.get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
				final String times = TimeUtil.formatSecondsL(time - System.currentTimeMillis() / 1000);
				return new ExtractedVideo(results, metadata, ExtractionResult.FAILED).setMessage("ERROR_UPCOMING_STREAM").setYTMessage(
						"This video/stream will be live in " + times + " from now. (Stream Start: " + Dates.time((long) (time * 1000)).toString() + ")");
			}
			logger.debug("Processing legacy map.");
			final UniJsonValue streamingData = player_response.getSafe("streamingData");
			final String legacy_map = map.get("url_encoded_fmt_stream_map");
			if (legacy_map != null) {
				final String[] legacyVideos = legacy_map.split(",");
				for (final String legacyVideo : legacyVideos) {
					final Map<String, String> vargs = LegacyURIUtils.queryStringToMap(legacyVideo);
					String vurl = vargs.get("url");
					if (vurl == null) {
						continue;
					}
					if (vargs.containsKey("sp")) {
						// encrypted.
						vurl += "?" + vargs.get("sp") + "=" + e.waitTillCompletion().decrypt(vargs.get("s"));
					} else {
						// not needed :)
					}
					final int itag = MathUtil.safeParseInt(vargs.get("itag"), -1);
					final Map<String, String> urlargs = LegacyURIUtils.queryStringToMap(vurl);
					results.add(new YouTubeVideo(vurl, false, itag, Formats.auto(itag, urlargs, vurl)).setCodecMime(vargs.get("type"))
							.setFileSize(MathUtil.safeParseL(urlargs.get("clen"), -1)));
					urlargs.clear();
					vargs.clear();
				}
			} else {
				final UniJsonIterableValue formats = streamingData.get("formats").asArray();
				if (!formats.isNull()) {
					for (final UniJsonValue _v : formats) {
						final UniJsonValue v = _v;
						String vurl = v.getString("url", null);
						if (v.contains("cipher", "signatureCipher")) {
							final Map<String, String> y = LegacyURIUtils
									.queryStringToMap(v.getStrings("signatureCipher", "cipher").replaceAll(Pattern.quote("\\u0026"), "&"));
							vurl = y.get("url");
							vurl += "&" + y.get("sp") + "=" + e.waitTillCompletion().decrypt(y.get("s"));
						}
						final int itag = v.getInt("itag");
						if (vurl.contains("&dur=0.000")) {
							logger.warn("A source had zero duration, skipped.");
							continue;
						}
						results.add(new YouTubeVideo(vurl, false, itag, Formats.get(itag)).setCodecMime(v.getString("mimeType"))
								.setFileSize(v.getLong("contentLength", -1)));
					}
				}
			}
			logger.debug("=> Processing new map.");
			final String new_map = map.get("adaptive_fmts");
			if (new_map != null) {
				final String[] newVideos = new_map.split(",");
				for (final String video : newVideos) {
					final Map<String, String> vargs = LegacyURIUtils.queryStringToMap(video);
					String vurl = vargs.get("url");
					if (vargs.containsKey("sp")) {
						// encrypted.
						vurl += "?" + vargs.get("sp") + "=" + e.waitTillCompletion().decrypt(vargs.get("s"));
					} else {
						// not needed :)
					}
					if (vurl.contains("&dur=0.000")) {
						logger.warn("A source had zero duration, skipped.");
						continue;
					}
					final int itag = MathUtil.safeParseInt(vargs.get("itag"), -1);
					// urlargs: NOT NEEDED
					results.add(new YouTubeVideo(vurl, true, itag, Formats.auto(itag, vargs, vurl)).setCodecMime(vargs.get("type"))
							.setFileSize(MathUtil.safeParseL(vargs.get("clen"), -1)).setSize(vargs.get("size")).setInitPayload(vargs.get("init"))
							.setIndexPayload(vargs.get("index")).setFPS(MathUtil.safeParseInt(vargs.get("fps"), 24))
							.setBitrate(MathUtil.safeParseInt(vargs.get("bitrate"), 80000)));
					vargs.clear();
				}
			} else {
				try {
					final UniJsonIterableValue formats = streamingData.get("adaptiveFormats").asArray();
					if (formats.isNull()) {
						logger.warn("Missing adaptiveFormats!");
						logger.warn(player_response.toString());
					} else {
						for (final UniJsonValue _v : formats) {
							final UniJsonValue v = _v;
							String vurl = v.getString("url", null);
							if (v.contains("cipher", "signatureCipher")) {
								final Map<String, String> y = LegacyURIUtils
										.queryStringToMap(v.getStrings("signatureCipher", "cipher").replaceAll(Pattern.quote("\\u0026"), "&"));
								vurl = y.get("url");
								vurl += "&" + y.get("sp") + "=" + e.waitTillCompletion().decrypt(y.get("s"));
							}
							final int itag = v.getInt("itag");
							if (vurl.contains("&dur=0.000")) {
								logger.warn("A source had zero duration, skipped.");
								continue;
							}
							results.add(new YouTubeVideo(vurl, true, itag, Formats.get(itag)).setCodecMime(v.getString("mimeType"))
									.setFileSize(v.getLong("contentLength", -1)).setSize(v.getInt("width", 0) + "x" + v.getInt("height", 0))
									.setInitPayload(v.get("initRange").getString("start") + "-" + v.get("initRange").getString("end"))
									.setIndexPayload(v.get("indexRange").getString("start") + "-" + v.get("indexRange").getString("end"))
									.setFPS(v.getInt("fps", -1)).setBitrate(v.getInt("bitrate", 80000)));
						}
					}
				} catch (Exception _e) {
					logger.error("Hit an error while processing an entry!", _e);
				}
			}
			if (player_response.hasValue("streamingData")) {
				if (streamingData.hasValue("hlsManifestUrl")) {
					logger.debug("Parsing a live video...");
					final String hlsMetadata = streamingData.getString("hlsManifestUrl");
					results.clear();
					results.add(new YouTubeVideo(hlsMetadata, false, 0, new Format(0, FileType.M3U8, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false))
							.setLivestream(true).setInternalProxy(true).setFormatType(FormatType.M3U8_STREAM));
					return new ExtractedVideo(results, metadata, ExtractionResult.SUCCESS);
				}
			}
			if (results.isEmpty()) {
				return new ExtractedVideo(results, metadata, ExtractionResult.FAILED_GEOGRAPHICALLY_BLOCKED).setMessage("ERROR_NO_VIDEOS_EXTRACTED")
						.setYTMessage("Failed to get any video source. Is this video geographically restricted?");
			}
			return new ExtractedVideo(results, metadata, ExtractionResult.SUCCESS).setPlaylist(pl);
		});
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

	@Override
	public String getID() {
		return "YTE.next.embedFEE";
	}

	@Override
	public boolean isLegacyBackend() {
		return false;
	}

	@Override
	public int abiVersion() {
		return 3450;
	}
}
